Skip to content

feat(bindgen): per-function async override for p3 (async_interfaces) (#526)#527

Merged
avrabe merged 1 commit into
mainfrom
feat/p3-per-function-async
Jun 18, 2026
Merged

feat(bindgen): per-function async override for p3 (async_interfaces) (#526)#527
avrabe merged 1 commit into
mainfrom
feat/p3-per-function-async

Conversation

@avrabe

@avrabe avrabe commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #526.

Problem

rust_wasm_component_bindgen(wasi_version = "p3") hardcoded --async all, so every export — including functions whose WIT signature is plain sync — was lifted async ([async-lift]… / [task-return]…). A call-return consumer can't invoke an async-lift export, so relay had to stand up a whole separate p2 sibling component just to expose one sync entry point for its witness MC/DC harness.

Change

Adds an async_interfaces parameter to rust_wasm_component_bindgen exposing wit-bindgen's --async filters on the guest bindings (native-guest stays sync — the host has no async_support). Default is unchanged:

wasi_version default async_interfaces
p3 ["all"]
p2 []

Callers can now mix sync and async exports:

async_interfaces = []                                       # WIT defaults: async-typed → async, sync → sync
async_interfaces = ["-all"]                                 # force every export sync
async_interfaces = ["-export:pkg:iface/i@0.1.0#fn"]         # that export sync, others WIT-default
async_interfaces = ["pkg:iface/i@0.1.0#stream-fn"]          # that export async, others WIT-default

For #526's exact case — keep stream exports async, lift one function sync — the cleanest answer is often async_interfaces = []: once the WIT distinguishes async-typed (stream/future) from sync functions, wit-bindgen's per-function default already does the right thing; it was only the blanket all forcing the sync ones async.

Also fixes _build_async_args to emit --async=VALUE instead of --async VALUE, so filter values starting with - (-all, -export:…) aren't misparsed by clap as separate flags (error: unexpected argument '-e').

Verification

New //test/p3:hello_p3_sync builds a p3 component whose greet export is implemented as a plain sync fn greet (no async, no #[cfg] split) using async_interfaces = ["-export:hello:interfaces/greeting@0.1.0#greet"]:

  • bazel test //test/p3:p3_sync_export_build_test passes.
  • 🔬 Falsification: under the p3 default (--async=all) the same sync impl fails to compile — method should be async or return a future, but it is synchronous — and the generated bindings carry 3 async-lift markers for greet; with the override they carry 0.
  • ✅ Default p3 path (//test/p3:hello_p3, --async=all) still builds — no regression from the arg-form change.
  • p2_build_test, p3_build_test still pass.

wit-bindgen 0.54 contract (documented in the rule)

  • Filter names must include the package version (pkg:iface/i@0.1.0#fn).
  • Combining the all blanket with a per-export -export: exclude is rejected (unused async option) — use [] (WIT defaults) or the allowlist form instead.

Refs: pulseengine/relay#202, pulseengine/witness#107

🤖 Generated with Claude Code

…526)

`rust_wasm_component_bindgen(wasi_version="p3")` hardcoded `--async all`, so
every export — including plain-sync WIT functions — was lifted async. A
call-return consumer (e.g. a witness MC/DC harness) cannot invoke an async-lift
export, forcing a separate p2 sibling component just to expose one sync entry
point (#526).

Add an `async_interfaces` parameter exposing wit-bindgen's `--async` filters on
the guest bindings. Default is unchanged (p3 -> ["all"], p2 -> []). Callers can
now pass:
  []                                   # no --async: each export follows its WIT
                                       #   signature (async-typed async, sync sync)
  ["-all"]                             # force every export sync
  ["-export:pkg:iface/i@0.1.0#fn"]     # that export sync, others WIT-default
  ["pkg:iface/i@0.1.0#stream-fn"]      # that export async, others WIT-default

Also fix `_build_async_args` to emit `--async=VALUE` (not `--async VALUE`) so
filter values beginning with `-` (`-all`, `-export:...`) aren't misparsed by
clap as separate flags.

Verified end-to-end: new //test/p3:hello_p3_sync builds a p3 component whose
`greet` export is a plain sync `fn` (no async, no cfg dance) via
async_interfaces=["-export:hello:interfaces/greeting@0.1.0#greet"]. Under the
p3 default this fails to compile ("method should be `async` or return a future,
but it is synchronous"); generated bindings drop from 3 async-lift exports to 0.
Default p3 path (hello_p3, --async=all) still builds — no regression.

Findings captured for the wit-bindgen 0.54 contract: filter names require the
package version; combining `all` with a per-export `-export:` exclude is
rejected ("unused async option") — use [] (WIT defaults) or the allowlist form.

Refs: pulseengine/relay#202, pulseengine/witness#107

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@avrabe avrabe merged commit d8cdb30 into main Jun 18, 2026
28 checks passed
@avrabe avrabe deleted the feat/p3-per-function-async branch June 18, 2026 05:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

p3 bindgen forces async: all — no per-function sync lift, blocking call-return access to a single export

1 participant